/*
 * Copyright (c) 2022 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/types.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>

#include <zephyr/sys/printk.h>
#include <zephyr/linker/sections.h>

#include <zephyr/ztest.h>
#include <zephyr/net/offloaded_netdev.h>
#include <zephyr/net/net_offload.h>
#include <zephyr/net/socket_offload.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_l2.h>

static struct in_addr test_addr_ipv4 = { { { 192, 0, 2, 1 } } };
static struct in6_addr test_addr_ipv6 = { { {
	0x20, 0x01, 0x0d, 0xb8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1
} } };

/* Dummy socket creator for socket-offloaded ifaces */
int offload_socket(int family, int type, int proto)
{
	return -1;
}

/* Dummy offload API for net-offloaded ifaces */
struct net_offload net_offload_api;

/* Dummy init function for socket-offloaded ifaces */
static void sock_offload_l2_iface_init(struct net_if *iface)
{
	/* This must be called, and the passed-in socket creator cannot be NULL,
	 * or the iface will not be recognized as offloaded
	 */
	net_if_socket_offload_set(iface, offload_socket);
	net_if_flag_set(iface, NET_IF_NO_AUTO_START);
	net_if_flag_set(iface, NET_IF_IPV4);
	net_if_flag_set(iface, NET_IF_IPV6);
}

/* Dummy init function for net-offloaded ifaces */
static void net_offload_l2_iface_init(struct net_if *iface)
{
	/* Reviewers: Is there a better way to do this?
	 * I couldn't find any actual examples in the source
	 */
	iface->if_dev->offload = &net_offload_api;
	net_if_flag_set(iface, NET_IF_NO_AUTO_START);
	net_if_flag_set(iface, NET_IF_IPV4);
	net_if_flag_set(iface, NET_IF_IPV6);
}

/* Tracks the total number of ifaces that are up (theoretically). */
atomic_t up_count = ATOMIC_INIT(0);

/* Tracks the total number of times that the offload_impl_enable callback was called. */
atomic_t call_count = ATOMIC_INIT(0);

/* Expected return value from offload_impl_enable */
atomic_t retval = ATOMIC_INIT(0);

/* Functionality under test */
static int offload_impl_enable(const struct net_if *iface, bool enabled)
{
	atomic_inc(&call_count);
	if (enabled) {
		atomic_inc(&up_count);
	} else {
		atomic_dec(&up_count);
	}
	return atomic_get(&retval);
}

/* Net-dev APIs for L2s with offloaded sockets, with and without .enable */
static struct offloaded_if_api sock_offloaded_impl_api = {
	.iface_api.init = sock_offload_l2_iface_init,
	.enable = offload_impl_enable
};

static struct offloaded_if_api sock_offloaded_no_impl_api = {
	.iface_api.init = sock_offload_l2_iface_init
};

/* Net-dev APIs for L2s that are net-offloaded, with and without .enable */
static struct offloaded_if_api net_offloaded_impl_api = {
	.iface_api.init = net_offload_l2_iface_init,
	.enable = offload_impl_enable
};

static struct offloaded_if_api net_offloaded_no_impl_api = {
	.iface_api.init = net_offload_l2_iface_init
};


/* Socket-offloaded netdevs, with and without .enable */
NET_DEVICE_OFFLOAD_INIT(sock_offload_test_impl, "sock_offload_test_impl",
			NULL, NULL, NULL, NULL,
			CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
			&sock_offloaded_impl_api, 0);

NET_DEVICE_OFFLOAD_INIT(sock_offload_test_no_impl, "sock_offload_test_no_impl",
			NULL, NULL, NULL, NULL,
			CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
			&sock_offloaded_no_impl_api, 0);

/* Net-offloaded netdevs, with and without .enable */
NET_DEVICE_OFFLOAD_INIT(net_offload_test_impl, "net_offload_test_impl",
			NULL, NULL, NULL, NULL,
			CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
			&net_offloaded_impl_api, 0);

NET_DEVICE_OFFLOAD_INIT(net_offload_test_no_impl, "net_offload_test_no_impl",
			NULL, NULL, NULL, NULL,
			CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
			&net_offloaded_no_impl_api, 0);

static void net_offloaded_netdev_before(void *fixture)
{
	ARG_UNUSED(fixture);

	/* Default to successful return value */
	atomic_set(&retval, 0);

	/* Reset all ifaces */
	net_if_down(NET_IF_GET(sock_offload_test_impl, 0));
	net_if_down(NET_IF_GET(sock_offload_test_no_impl, 0));
	net_if_down(NET_IF_GET(net_offload_test_impl, 0));
	net_if_down(NET_IF_GET(net_offload_test_impl, 0));

	/* Reset counters */
	atomic_set(&call_count, 0);
	atomic_set(&up_count, 0);
}

ZTEST(net_offloaded_netdev, test_up_down_sock_off_impl)
{
	struct net_if *test_iface = NET_IF_GET(sock_offload_test_impl, 0);

	/* Verify iface under test is down before test */
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test must be admin-down before test");

	/* Bring iface up. */
	(void)net_if_up(test_iface);

	/* Verify that a single iface went up once (according to the enable callback) */
	zassert_equal(atomic_get(&call_count), 1,
			"Bad transition-count, offload_impl_enable not called correctly");
	zassert_equal(atomic_get(&up_count), 1,
			"Bad up-count, offload_impl_enable not called correctly");
	zassert_true(net_if_is_admin_up(test_iface),
			"Iface under test should be up after net_if_up");

	/* Bring iface down */
	(void)net_if_down(test_iface);

	/* Verify that a single iface went down once (according to the enable callback)*/
	zassert_equal(atomic_get(&call_count), 2,
			"Bad transition-count, offload_impl_enable not called correctly");
	zassert_equal(atomic_get(&up_count), 0,
			"Bad up-count, offload_impl_enable not called correctly");
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test should be down after net_if_down");
}

ZTEST(net_offloaded_netdev, test_up_down_sock_off_no_impl)
{
	struct net_if *test_iface = NET_IF_GET(sock_offload_test_no_impl, 0);

	/* Verify iface under test is down before test */
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test must be admin-down before test");

	/* Bring iface up */
	(void)net_if_up(test_iface);

	/* Verify that the iface went up, but callbacks were not fired*/
	zassert_equal(atomic_get(&call_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_equal(atomic_get(&up_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_true(net_if_is_admin_up(test_iface),
			"Iface under test should be up after net_if_up");

	/* Bring iface down */
	(void)net_if_down(test_iface);

	/* Verify that the iface went down, but callbacks were not fired*/
	zassert_equal(atomic_get(&call_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_equal(atomic_get(&up_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test should be down after net_if_down");
}

ZTEST(net_offloaded_netdev, test_up_down_net_off_impl)
{
	struct net_if *test_iface = NET_IF_GET(net_offload_test_impl, 0);

	/* Verify iface under test is down before test */
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test must be admin-down before test");

	/* Bring iface up. */
	(void)net_if_up(test_iface);

	/* Verify that a single iface went up once (according to the enable callback) */
	zassert_equal(atomic_get(&call_count), 1,
			"Bad transition-count, offload_impl_enable not called correctly");
	zassert_equal(atomic_get(&up_count), 1,
			"Bad up-count, offload_impl_enable not called correctly");
	zassert_true(net_if_is_admin_up(test_iface),
			"Iface under test should be up after net_if_up");

	/* Bring iface down */
	(void)net_if_down(test_iface);

	/* Verify that a single iface went down once (according to the enable callback)*/
	zassert_equal(atomic_get(&call_count), 2,
			"Bad transition-count, offload_impl_enable not called correctly");
	zassert_equal(atomic_get(&up_count), 0,
			"Bad up-count, offload_impl_enable not called correctly");
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test should be down after net_if_down");
}

ZTEST(net_offloaded_netdev, test_up_down_net_off_no_impl)
{
	struct net_if *test_iface = NET_IF_GET(net_offload_test_no_impl, 0);

	/* Verify iface under test is down before test */
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test must be admin-down before test");

	/* Bring iface up */
	(void)net_if_up(test_iface);

	/* Verify that the iface went up, but callbacks were not fired*/
	zassert_equal(atomic_get(&call_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_equal(atomic_get(&up_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_true(net_if_is_admin_up(test_iface),
			"Iface under test should be up after net_if_up");

	/* Bring iface down */
	(void)net_if_down(test_iface);

	/* Verify that the iface went down, but callbacks were not fired*/
	zassert_equal(atomic_get(&call_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_equal(atomic_get(&up_count), 0,
			"offload_impl_enable was called unexpectedly");
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test should be down after net_if_down");
}

ZTEST(net_offloaded_netdev, test_up_down_sock_off_impl_double)
{
	struct net_if *test_iface = NET_IF_GET(sock_offload_test_impl, 0);

	/* Verify iface under test is down before test */
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test must be admin-down before test");

	/* Bring iface up twice */
	(void)net_if_up(test_iface);
	(void)net_if_up(test_iface);

	/* Verify that a single iface went up once (according to the enable callback)*/
	zassert_equal(atomic_get(&call_count), 1,
			"Bad transition-count, offload_impl_enable not called correctly");
	zassert_equal(atomic_get(&up_count), 1,
			"Bad up-count, offload_impl_enable not called correctly");
	zassert_true(net_if_is_admin_up(test_iface),
			"Iface under test should be up after net_if_up");

	/* Verify that a single iface went down once (according to the enable callback)*/
	(void)net_if_down(test_iface);
	(void)net_if_down(test_iface);

	/* Verify appropriate calls were made */
	zassert_equal(atomic_get(&call_count), 2,
			"Bad transition-count, offload_impl_enable not called correctly");
	zassert_equal(atomic_get(&up_count), 0,
			"Bad up-count, offload_impl_enable not called correctly");
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test should be down after net_if_down");
}

ZTEST(net_offloaded_netdev, test_up_down_sock_off_impl_fail_up)
{
	struct net_if *test_iface = NET_IF_GET(sock_offload_test_impl, 0);

	/* Verify iface under test is down before test */
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test must be admin-down before test");

	/* Instruct the enable callback to fail */
	atomic_set(&retval, -E2BIG);

	/* Expect net_if_up to fail accordingly */
	zassert_equal(net_if_up(test_iface), -E2BIG,
		"net_if_up should forward error returned from offload_impl_enabled");

	/* Verify that the iface failed to go up */
	zassert_false(net_if_is_admin_up(test_iface),
			"Iface under test should have failed to go up");
}

ZTEST(net_offloaded_netdev, test_up_down_sock_off_impl_fail_down)
{
	struct net_if *test_iface = NET_IF_GET(sock_offload_test_impl, 0);

	/* Bring iface up before test */
	(void) net_if_up(test_iface);

	/* Instruct the enable callback to fail */
	atomic_set(&retval, -EADDRINUSE);


	/* Expect net_if_down to fail accordingly */
	zassert_equal(net_if_down(test_iface), -EADDRINUSE,
		"net_if_down should forward error returned from offload_impl_enabled");

	/* Verify that the iface failed to go down */
	zassert_true(net_if_is_admin_up(test_iface),
			"Iface under test should have failed to go up");
}

static void test_addr_add_common(struct net_if *test_iface, const char *off_type)
{
	struct net_if *lookup_iface;
	struct net_if_addr *ipv4_addr;
	struct net_if_addr *ipv6_addr;

	/* Bring iface up before test */
	(void)net_if_up(test_iface);

	ipv4_addr = net_if_ipv4_addr_add(test_iface, &test_addr_ipv4,
					 NET_ADDR_MANUAL, 0);
	zassert_not_null(ipv4_addr,
			"Failed to add IPv4 address to a %s offloaded interface",
			off_type);
	ipv6_addr = net_if_ipv6_addr_add(test_iface, &test_addr_ipv6,
					 NET_ADDR_MANUAL, 0);
	zassert_not_null(ipv6_addr,
			 "Failed to add IPv6 address to a socket %s interface",
			 off_type);

	lookup_iface = NULL;
	zassert_equal_ptr(net_if_ipv4_addr_lookup(&test_addr_ipv4, &lookup_iface),
			  ipv4_addr,
			  "Failed to find IPv4 address on a %s offloaded interface", off_type);
	zassert_equal_ptr(lookup_iface, test_iface, "Wrong interface");

	lookup_iface = NULL;
	zassert_equal_ptr(net_if_ipv6_addr_lookup(&test_addr_ipv6, &lookup_iface),
			  ipv6_addr,
			  "Failed to find IPv6 address on a %s offloaded interface", off_type);
	zassert_equal_ptr(lookup_iface, test_iface, "Wrong interface");

	zassert_true(net_if_ipv4_addr_rm(test_iface, &test_addr_ipv4),
		     "Failed to remove IPv4 address from a %s offloaded interface",
		     off_type);
	zassert_true(net_if_ipv6_addr_rm(test_iface, &test_addr_ipv6),
		     "Failed to remove IPv4 address from a %s offloaded interface",
		     off_type);
}

ZTEST(net_offloaded_netdev, test_addr_add_sock_off_impl)
{
	struct net_if *test_iface = NET_IF_GET(sock_offload_test_impl, 0);

	test_addr_add_common(test_iface, "offloaded");
}

ZTEST(net_offloaded_netdev, test_addr_add_net_off_impl)
{
	struct net_if *test_iface = NET_IF_GET(net_offload_test_impl, 0);

	test_addr_add_common(test_iface, "net");
}

static bool offload_getaddrinfo_called;

static int test_offload_getaddrinfo(const char *node,
				    const char *service,
				    const struct zsock_addrinfo *hints,
				    struct zsock_addrinfo **res)
{
	ARG_UNUSED(node);
	ARG_UNUSED(service);
	ARG_UNUSED(hints);
	ARG_UNUSED(res);

	offload_getaddrinfo_called = true;

	return 0;
}

static void test_offload_freeaddrinfo(struct zsock_addrinfo *res)
{
	ARG_UNUSED(res);
}

static const struct socket_dns_offload test_dns_offload_ops = {
	.getaddrinfo = test_offload_getaddrinfo,
	.freeaddrinfo = test_offload_freeaddrinfo,
};

ZTEST(net_offloaded_netdev, test_dns_offload)
{
	struct zsock_addrinfo *ai;

	/* Register offloaded DNS */
	offload_getaddrinfo_called = false;
	socket_offload_dns_register(&test_dns_offload_ops);
	zassert_true(socket_offload_dns_is_enabled(),
		     "DNS offloading should be enabled");
	zassert_ok(zsock_getaddrinfo("127.0.0.1", NULL, NULL, &ai),
		   "getaddrinfo() failed");
	zassert_true(offload_getaddrinfo_called,
		     "Offloaded implementation should be called");
	zsock_freeaddrinfo(ai);

	/* Disable offloaded DNS */
	offload_getaddrinfo_called = false;
	socket_offload_dns_enable(false);
	zassert_false(socket_offload_dns_is_enabled(),
		     "DNS offloading should be disabled");
	zassert_ok(zsock_getaddrinfo("127.0.0.1", NULL, NULL, &ai),
		   "getaddrinfo() failed");
	zassert_false(offload_getaddrinfo_called,
		     "Offloaded implementation shouldn't be called");
	zsock_freeaddrinfo(ai);

	/* Reenable offloaded DNS */
	offload_getaddrinfo_called = false;
	socket_offload_dns_enable(true);
	zassert_true(socket_offload_dns_is_enabled(),
		     "DNS offloading should be enabled");
	zassert_ok(zsock_getaddrinfo("127.0.0.1", NULL, NULL, &ai),
		   "getaddrinfo() failed");
	zassert_true(offload_getaddrinfo_called,
		     "Offloaded implementation should be called");
	zsock_freeaddrinfo(ai);

	/* Deregister offloaded DNS */
	offload_getaddrinfo_called = false;
	socket_offload_dns_deregister(&test_dns_offload_ops);
	zassert_false(socket_offload_dns_is_enabled(),
		      "DNS offloading should be disabled");
	zassert_ok(zsock_getaddrinfo("127.0.0.1", NULL, NULL, &ai),
		   "getaddrinfo() failed");
	zassert_false(offload_getaddrinfo_called,
		     "Offloaded implementation shouldn't be called");
	zsock_freeaddrinfo(ai);
}

ZTEST_SUITE(net_offloaded_netdev, NULL, NULL, net_offloaded_netdev_before, NULL, NULL);
